배경
three-mesh-bvh는 한동안 사용할 것 같은데 bvh 생성시간과 메인 스레드에 걸리는 부하를 줄이기 위해 GPGPU를 슬쩍 해보고 싶었는데 WebGPU에 대해 잘 모르고 있어서 WebGPU 간단한 튜토리얼 따라가보려고 했습니다.
필요 세팅
Chrome에서 기본적으로 WebGPU 활성화가 안되어있는데 이를 활성화해주긴 위해선 https://를 통해 제공하거나 localhost의 경우는 —unsafely-treat-insecure-origin-as-secure 옵션에서 예외처리를 해주어야한다.
chrome://gpu 를 통해 WebGPU 관련 사항들을 볼 수 있음
WebGPU는 현재 Experimental이라 types에 대한 정의가 부족합니다.
npmnpm: @webgpu/types
npm: @webgpu/types
This package defines Typescript types (`.d.ts`) for the upcoming [WebGPU standard](https://github.com/gpuweb/gpuweb/wiki/Implementation-Status).. Latest version: 0.1.60, last published: 6 days ago. Start using @webgpu/types in your project by running `npm i @webgpu/types`. There are 107 other projects in the npm registry using @webgpu/types.
typescript 사용시 위의 패키지 이용해줘야됩니다.
WebGPU를 이용한 GPGPU(General-Purpose computing on Graphics Processing Units)
WebGPU 오브젝트의 label
label은 선택사항이지만 붙여주는 것을 권장, 대부분의 에러가 label과 함께 출력되기 때문
async function mainGPGPU() { const adapter = await navigator.gpu?.requestAdapter(); const device = await adapter?.requestDevice(); //Adapter는 실제 GPU 하드웨어를 가리킵니다. //Device는 GPU 명령어를 실행할 주체 if (!device) { alert('need a browser that supports webgpu'); return; } //연산을 위한 Shader Module 생성 const module = device.createShaderModule({ label: 'doubling compute module', code: ` @group(0) @binding(0) var<storage, read_write> data: array<f32>; @compute @workgroup_size(1) fn computeSomething( @builtin(global_invocation_id) id: vec3u ) { let i = id.x; data[i] = data[i] * 2.0; } `, }); //연산을 위한 Pipeline 생성 const pipeline = device.createComputePipeline({ label: 'doubling compute pipeline', layout: 'auto', compute: { module, }, }); // 일반 실행시간과 GPGPU 실행시간 비교 const input = new Float32Array(10000); const input2 = new Float32Array(10000); // 데이터 초기화 10000개 for (let i = 0; i < 10000; i++) { input[i] = i; input2[i] = i; } const output = new Float32Array(10000); // 일반 실행 for (let i = 0; i < 10000; i++) { output[i] = input[i] * 2; } // create a buffer on the GPU to hold our computation // input and output const workBuffer = device.createBuffer({ label: 'work buffer', size: input2.byteLength, // STORAGE를 선언해줘야 Shader에 선언한 var<storage, read_write> data: array<f32> 호환된다. // 버퍼에 복사와 버퍼로부터 데이터 복사를 원하기 때문에 플래그 포함 usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, }); // Copy our input data to that Buffer device.queue.writeBuffer(workBuffer, 0, input2); // js는 WebGPU 버퍼를 직접 읽을 수 없다는 점에 유의할것 // js에서 매핑 가능한 WebGPU 버퍼는 다른 용도로 사용할 수 없다. // create a buffer on the GPU to get a copy of the results const resultBuffer = device.createBuffer({ label: 'result buffer', size: input2.byteLength, // MAP_READ 플래그를 포함하면 버퍼를 읽을 수 있다. usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, }); // Setup a bindGroup to tell the shader which buffer to use for computation const bindGroup = device.createBindGroup({ label: 'bindGroup for work buffer', layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: workBuffer } }], }); // Encode commands to do the computation // Command를 Command Buffer에 인코딩해줄 Encoder const encoder = device.createCommandEncoder({ label : 'doubling encoder'}); const pass = encoder.beginComputePass({ label: 'doubling compute pass'}); pass.setPipeline(pipeline); pass.setBindGroup(0, bindGroup); pass.dispatchWorkgroups(input2.length); pass.end(); encoder.copyBufferToBuffer(workBuffer, 0, resultBuffer, 0, resultBuffer.size); // GPU에 제출할 CommandBuffer 완성 const commandBuffer = encoder.finish(); // 실제 커맨드 버퍼를 제출하기 전까진 실행되는 개념이 X // submit 해줘야 실행 device.queue.submit([commandBuffer]); await resultBuffer.mapAsync(GPUMapMode.READ); const result = new Float32Array(resultBuffer.getMappedRange()); // unmap 하게 되면 getMappedRange를 통해 생성된 arrayBuffer는 싹 비워지게 됩니다. resultBuffer.unmap(); }
대부분의 시간이 mapAsync 의 시간입니다.
덤
// Render를 위해서는 context를 configure해야 되는데 // 해당 getPreferredCanvasFormat 함수를 불러주는게 좋습니다.(권장) // 그렇지 않게 되면 Canvas로 Texture를 렌더링해줄때 추가적인 복사 등의 오버헤드가 발생 const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device, format: presentationFormat, });
향후 계획
WebGPU Samples
The WebGPU Samples are a set of samples demonstrating the use of the WebGPU API.
three.js/examples/webgpu_compute_audio.html at master · mrdoob/three.js
JavaScript 3D Library. Contribute to mrdoob/three.js development by creating an account on GitHub.
샘플 들 중에 써먹을 만한 부분 찾아서 추가 리서치 예정
Three.js WebGPU 샘플도 참고
참조
Red Triangle 그리기
GPU를 이용하여 하드코딩하여 그린 삼각형

async function main() { const adapter = await navigator.gpu?.requestAdapter(); const device = await adapter?.requestDevice(); if(!device){ alert('need a browser that supports webgpu'); return; } const canvas = document.querySelector('canvas')!; const context = canvas.getContext('webgpu')as unknown as GPUCanvasContext; if(!canvas) { throw new Error('canvas not found'); } if(!context) { throw new Error('webgpu context not found'); } const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device, format: presentationFormat, }); //Shader Module에는 Vertex, Fragment 두 개의 셰이더가 추가 const module = device.createShaderModule({ label : 'our hardcoded red triangle shaders', code : ` @vertex fn vs( @builtin(vertex_index) vertexIndex : u32) -> @builtin(position) vec4f { let pos = array( vec2f( 0.0, 0.5), // top center vec2f(-0.5, -0.5),// bottom left vec2f(0.5, -0.5) // bottom right ); return vec4f(pos[vertexIndex], 0.0, 1.0); } @fragment fn fs() -> @location(0) vec4f { return vec4f(1.0, 0.0, 0.0, 1.0); } ` }); // entryPoint를 명시하지 않은 이유는 module에 각 함수가 1개만 존재하기 때문 const pipeline = device.createRenderPipeline({ label : 'our hardcoded red triangle pipeline', layout : 'auto', vertex : { module, }, fragment:{ module, targets: [{format: presentationFormat}], }, }); const renderPassDescriptor : GPURenderPassDescriptor = { label: 'our basic canvas renderPass', colorAttachments:[ { //view : <- to be filled out when we render 렌더할 때 채워넣는다. clearValue: [0.3, 0.3, 0.3, 1], loadOp:'clear', storeOp:'store', }, ], }; function render(){ //Get the current texture from the canvas context and // set it as the texture to render to. renderPassDescriptor.colorAttachments[0].view = context.getCurrentTexture().createView(); // make a command encoder to start encoding commands const encoder = device!.createCommandEncoder({label: 'our encoder'}); //make a render pass encoder to encode render specific commands const pass = encoder.beginRenderPass(renderPassDescriptor); pass.setPipeline(pipeline); pass.draw(3); // call our vertex shader 3 times pass.end(); const commandBuffer = encoder.finish(); device!.queue.submit([commandBuffer]); } render(); } main().then(()=> { console.log('success'); }).catch((e)=> { console.error(e); });
참조
Difference between WebGL, WebGPU